﻿///<author>
///Zachary Haberman
///</author>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace HistogramPackage {

    struct Bucket {
        private int _bucketCount;
        private decimal _frequency;

        public int BucketCount { get { return _bucketCount; } }
        public decimal Frequency { get { return _frequency; } }
        
        public Bucket(int bucketCount, int totalCount) {
            _bucketCount = bucketCount;
            _frequency = (decimal)bucketCount / (decimal)totalCount;
        }
    }

    class Histogram {
        
        private int _bucketCount;
        private double _start;
        private double _end;
        private double _min;
        private double _max;
        private int _count;
        private double _barWidth;
        private double _histogramWidth;
        private Dictionary<double, int> _dataSet;
        private int _maxDecimalCount;
        private int _negativeHandle;
        private double[] _boundaries;
        private Bucket[] _buckets;
        private Dictionary<string, Bitmap> _histograms;
        private Color _bucketColor;

        private const int DATA_POINTS = 10000;
        private const int OFFSET_MINORA = 2;
        private const int OFFSET_MEDIA = 20;
        private const int OFFSET_MAJORA = OFFSET_MINORA + OFFSET_MEDIA;



        public enum NegativeHandle { TRUNCATE = 0, SHIFT = 1 };
        
        public int BucketCount { set { resetBucketCount(value); } get { return _bucketCount; } }
        public Dictionary<double, int> DataSet { get { return _dataSet;} }
        public double Start { get { return _start; } }
        public double End { get { return _end; } }
        public int MaxDecimalCount { get { return _maxDecimalCount; } }
        public int NegativeHandleValue { get { return _negativeHandle; } }
        public double BarWidth { get { return _barWidth; } }
        public double HistogramWidth { get { return _histogramWidth; } }
        public int DataSetCount { get { return _count; } }
        public Bucket[] Buckets { get { return _buckets; } }


        /// <summary>
        /// The Constructor for the Histogram class. This builds the histogram for the 
        /// </summary>
        /// <param name="dataSet">The data set, which should be a dictionary of a double, int pair.</param>
        /// <param name="sorted">if the data set is already sorted, set this to true. Otherwise, it will sort the data set.</param>
        public Histogram(List<Tuple<double, int>> dataSet, bool sorted = false, int bucketCount = 40, NegativeHandle handle = 0) {
            _negativeHandle = (int)handle;

            var newDataSet = new Dictionary<double, int>();

            foreach (var tuple in dataSet) {
                int tValue;
                if (newDataSet.TryGetValue(tuple.Item1, out tValue)) {
                    newDataSet[tuple.Item1] += 1;
                }
                else {
                    newDataSet.Add(tuple.Item1, tuple.Item2);
                }
            }



            if (!sorted) {
                _dataSet = sortDictionary(newDataSet);
            }
            else {
                _dataSet = newDataSet;
            }

            //Setting the maximum Decimal Count. It is gauranteed to be at least 1.
            _count = getValuesCount(_dataSet);
            _bucketColor = Color.Blue;
            _maxDecimalCount = getHighestDecimalCount(_dataSet);
            resetBucketCount(bucketCount);
        }
        
        /// <summary>
        /// The Constructor for the Histogram class. This builds the histogram for the 
        /// </summary>
        /// <param name="dataSet">The data set, which should be a dictionary of a double, int pair.</param>
        /// <param name="sorted">if the data set is already sorted, set this to true. Otherwise, it will sort the data set.</param>
        public Histogram(Dictionary<double, int> dataSet, bool sorted = false, int bucketCount = 40, NegativeHandle handle = 0) {
            _negativeHandle = (int)handle;
            
            if(!sorted) {
                _dataSet = sortDictionary(dataSet);
            }
            else {
                _dataSet = dataSet;
            }

            //Setting the maximum Decimal Count. It is gauranteed to be at least 1.
            _count = getValuesCount(_dataSet);
            _bucketColor = Color.Blue;
            _maxDecimalCount = getHighestDecimalCount(_dataSet);
            resetBucketCount(bucketCount);
        }

        /// <summary>
        /// This sorts the dictionary based on its Keys.
        /// </summary>
        /// <param name="dataSet">The data set to sort</param>
        /// <returns>Returns a dictionary sorted by the Keys, in ascending order.</returns>
        private Dictionary<double, int> sortDictionary(Dictionary<double, int> dataSet) {
            
            var sortedDataSet = new Dictionary<double, int>();
            
            var items = from k in dataSet.Keys
                        orderby k ascending
		                select k;
            

            foreach (var item in items) {
                if (_negativeHandle == 0 && item >= 0) {
                    sortedDataSet.Add(item, dataSet[item]);
                }
                else if (_negativeHandle == 1 && _min < 0) {
                    sortedDataSet.Add(item + (-_min), dataSet[item]);
                }
                else if (_negativeHandle == 1 && _min >= 0) {
                    sortedDataSet.Add(item, dataSet[item]);
                }
            }

            _min = sortedDataSet.First().Key;
            _max = sortedDataSet.Last().Key;

            items = null;
            dataSet.Clear();

            return sortedDataSet;
        }

        /// <summary>
        /// Returns the longest running decimal point in the data set.
        /// </summary>
        /// <param name="dataSet">The data set to be examined.</param>
        /// <returns>Returns an integer value representing the length of the longest decimal</returns>
        private int getHighestDecimalCount(Dictionary<double, int> dataSet) {
            int maxDecimalCount = 0;
            
            foreach(KeyValuePair<double, int> kvp in dataSet) {
                string[] decCount = (kvp.Key.ToString()).Split('.');
                if (decCount.Length > 1) { maxDecimalCount = decCount[1].Length > maxDecimalCount ? decCount[1].Length : maxDecimalCount; }
            }

            return maxDecimalCount == 0 ? 1 : maxDecimalCount;
        }

        /// <summary>
        /// Gets the starting point for the buckets. 
        /// It staggers the start in order to avoid data points landing in between two buckets.
        /// </summary>
        /// <param name="min">The minimum value in the data set.</param>
        /// <param name="maxDecimalCount">The integer value representing the longest decimal in the data set.</param>
        /// <returns>Returns the starting point for the buckets.</returns>
        private double getStart(double min, int maxDecimalCount) {
            return min - (0.5 / (10 * maxDecimalCount));
        }

        /// <summary>
        /// Gets the ending point for the buckets.
        /// It staggers the end in order to avoid data points landing in between two buckets.
        /// </summary>
        /// <param name="max">The maximum value in the data set.</param>
        /// <param name="maxDecimalCount">The integer value representing the longest decimal in the data set.</param>
        /// <returns>Returns the ending point for the buckets.</returns>
        private double getEnd(double max, int maxDecimalCount) {
            return max + (0.5 / (10 * maxDecimalCount));
        }

        /// <summary>
        /// Calculates the widths of the histogram and of the barWidth.
        /// </summary>
        /// <param name="start"></param>
        /// <param name="end"></param>
        /// <param name="barCount"></param>
        /// <returns></returns>
        private double[] calculateWidths(double start, double end, int barCount) {
            double histogramWidth = end - start;
            if (histogramWidth < barCount) { 
                barCount = (int)Math.Round(histogramWidth, MidpointRounding.AwayFromZero);
                _bucketCount = barCount;
            }
            
            double barWidth = (double)histogramWidth / (double)barCount;
            histogramWidth = barWidth * (double)_bucketCount;
            return new double[] {histogramWidth, barWidth};
        }

        private double[] setBoundaries(double min, int bucketCount, double barWidth) {
            double[] boundaries = new double[bucketCount + 1];
            for (int i = 0; i < bucketCount + 1; i++) {
                boundaries[i] = min + i * barWidth;
            }

            return boundaries;

        }

        private int getValuesCount(Dictionary<double, int> dataSet) {

            int count = 0;
            foreach (var kvp in dataSet) {
                count += kvp.Value;
            }

            return count;
        }

        private Bucket[] setBuckets(int bucketCount, double[] boundaries, Dictionary<double, int> dataSet, int count) {

            Bucket[] buckets = new Bucket[bucketCount];
            int counter = 0;
            int localCount = 0;
            foreach (var kvp in dataSet) {
                if (kvp.Key < boundaries[counter + 1]) {
                    localCount += kvp.Value;
                }
                else {
                    buckets[counter] = new Bucket(localCount, count);
                    localCount = 0;
                    counter++;
                    
                }
            }
            

            return buckets;
        }

        private void resetBucketCount(int bucketCount) {
            _bucketCount = bucketCount;
            _start = getStart(_min, _maxDecimalCount);
            _end = getEnd(_max, _maxDecimalCount);
            double[] width = calculateWidths(_start, _end, _bucketCount);
            _histogramWidth = width[0];
            _barWidth = width[1];
            _boundaries = setBoundaries(_min, _bucketCount, _barWidth);
            _buckets = setBuckets(_bucketCount, _boundaries, _dataSet, _count);
            if (_histograms == null) {
                _histograms = new Dictionary<string, Bitmap>();
            }
        }

        private Bitmap createNewNistogram(string name, Size size, PDF pdf = null, int lineThickness = 2) {
            
            Graphics g;
            Bitmap b = new Bitmap(size.Width, size.Height);
            
            //Adding the bars.
            g = Graphics.FromImage(b);
            g.SmoothingMode = SmoothingMode.HighQuality;
            Pen lines = new Pen(Color.Black, 2f);
            
            //Vertical line
            g.DrawLine(lines, OFFSET_MEDIA, size.Height - OFFSET_MEDIA, size.Width - OFFSET_MEDIA, size.Height - OFFSET_MEDIA);
            
            //Horizontal line
            g.DrawLine(lines, OFFSET_MEDIA, OFFSET_MEDIA, OFFSET_MEDIA, size.Height - OFFSET_MEDIA);

            int bucketWidths = ((size.Width - OFFSET_MINORA) / (_bucketCount + 1)) - 1;
            int nextXLoc = ((((size.Width - OFFSET_MINORA) / _bucketCount) - bucketWidths) / 2) + OFFSET_MAJORA;
            decimal maxF = _buckets.Max(max => max.Frequency);
            double peak = .00;
            while (maxF > (decimal)peak) { peak += .0625; };

            if (peak == 0.0) { peak = 0.625; };
            //g.DrawLine(lines, 2, size.Height - , 6, (float)(size.Height * peak));
            
            for (int i = 0; i < _bucketCount; i++) {
                
                
                int bucketHeight = (int)((_buckets[i].Frequency * (size.Height - OFFSET_MAJORA))/(decimal)peak);
                nextXLoc += bucketWidths + 1;
                Rectangle r = new Rectangle(nextXLoc, size.Height - bucketHeight - OFFSET_MAJORA, bucketWidths, bucketHeight);
                if (r.Height != 0 && r.Width != 0) {
                    LinearGradientBrush brush = new LinearGradientBrush(r, _bucketColor, Color.LightBlue, .05f);
                    g.FillRectangle(brush, r);
                    g.DrawRectangle(new Pen(Brushes.Black, 1f), r);
                }
            }
            

            if (pdf != null) {

                int[] pdfVals = new int[DATA_POINTS];
                double range = _end - _start;
                int counter = 0;
                decimal rStep = (decimal)range / (decimal)DATA_POINTS;
                int fStep = (int)Math.Round((double)DATA_POINTS / nextXLoc, MidpointRounding.AwayFromZero);
                int currentStep = 0;
                int currentLoc = 0;
                nextXLoc = ((((size.Width - OFFSET_MINORA) / _bucketCount) - bucketWidths) / 2) + OFFSET_MAJORA;

                for (decimal d = (decimal)_start; d < (decimal)_end - (decimal).000001; d += rStep) {

                    int pointHeight = (int)((pdf.getOutput((double)d)) * (size.Height - OFFSET_MAJORA) / peak);
                    pdfVals[counter++] = pointHeight;
                }

                for (int i = 0; i < DATA_POINTS; i++) {
                    if (currentStep == fStep) {
                        currentStep = 0;
                        currentLoc++;
                    }
                    
                    g.FillRectangle(Brushes.Black, nextXLoc + currentLoc, size.Height - pdfVals[i] - OFFSET_MAJORA, lineThickness, lineThickness);
                    currentStep++;

                }
            }



            g.Dispose();
            _histograms.Add(name, b);
            
            return b;
            
           
        }

        public Bitmap GetHistogramWithBucketCount(Size size, int bucketCount, PDF pdf = null, int lineThickness = 2) {
            resetBucketCount(bucketCount);
            return GetHistogramBitmap(size, pdf, lineThickness);
        }

        public Bitmap GetHistogramBitmapWithColor(Size size, Color color, PDF pdf = null, int lineThickness = 2) {
            _bucketColor = color;
            return GetHistogramBitmap(size, pdf, lineThickness);
        }

        public Bitmap GetHistogramBitmap(Size size, PDF pdf = null, int lineThickness = 2) {
            string histoName = "";
            
            if (pdf == null) {
                histoName = String.Format("Standard-{0}-{1}-{2}-{3}-{4}", size.Width, size.Height, _bucketCount, lineThickness, _bucketColor.ToString());
            }
            else {
                histoName = String.Format("{0}-{1}-{2}-{3}-{4}-{5}", pdf.name, size.Width, size.Height, _bucketCount, lineThickness, _bucketColor.ToString());
            }

            Bitmap histo = null;
            if (_histograms.TryGetValue(histoName, out histo)) {
                return histo;
            }
            else {
                return createNewNistogram(histoName, size, pdf, lineThickness);
            }
            
        }

    }
}
